Цифровая подпись документов в Эстонии с помощью DigiDocService
В Эстонии с 2000 года вступил в силу закон о цифровых подписях, которые стали юридически равноценны обычным рукописным. Вскоре была создана и техническая основа - компания SertifitseerimisKeskus (буквально - «центр сертификации») принадлежащая банкам и телекоммуникацонным операторам (а не государству, представляете себе!) и схема обмена данными по X.509 стандарту. Эта статья расчитана в большей мере на программистов.
Цифровая подпись?
Подпись как оказывается очень важна, а признаваемая государством - тем более. Снижаются затраты на распечатку и/или доставку счетов по оплате, договоров между работником и работодателем. Я уже не говорю про обычное подтверждение что документ прислан точно нужным человеком, а не хакером. Спасает положение то что у каждого гражданина Эстоини есть сертификат подписи, но его недостаточно. Проблема в том что одной подписи-закарючки в IT-мире недостаточно. Подпись в расширенном виде на самом деле включает в себя набор данные, в том числе не статичные.
-
Стороны подписывающие документ
-
Собственно документ или его отпечаток (говорящий о неизменном состоянии со времени подписания)
-
Свидетели (нотариус) и роль сторон
-
Время, место
Контейнер всей этой информации решили сделать на XML и назвать .ddoc расширением и связать с онлайн-сервисом создания и подтверждения подписей — Digidoc. За основу берутся основные свойства эстонской ID-карточки - авторизация, подпись и шифрование и в результате имеем:
- цифровая подпись файлов (DigiDoc клиент, портал или третья сторона через DigiDocService)
- шифрование и дешифрование файлов (DigiDoc клиент)
- подтверждение действительности (digidoccheck)
- подпись электронной почты
- подпись или авторизация с помощью мобильного телефона (Mobiil-ID)
Контейнер со времени создания претерпел некоторые изменения, сейчас есть версия 1.3 основана на стандарте XAdES-X-L расширенных электронных подписей.
Процесс создания подписи с DigiDocService
Теперь собственно о главном что может понадобится на любом сайте. Допустим вы продаёте рога и копыта и хотите всё юридически правильно оформить. По-старинке это было бы типичный checkbox мол «согласен с условиями». Теперь же можно получить юридически действительную подпись клиента под любым договором, распиской купли-продажи или договора предоставления услуги.
Что-бы это у себя сделать Sertifitseerimiskeskus предоставляет услугу DigiDocService по SOAP, и для этого опубликованы следующие списки WSDL-методов: http://www.sk.ee/DigiDocService/DigiDocService_2_3.wsdl //почти live https://digidocservice.sk.ee/?wsdl //live - работает с CURL только вместе с Juur-SK.crt https://www.openxades.org:8443/?wsdl //test
В обмене данными участвуют следующие стороны
-
Клиент с нормальной ид-карточкой и софтом
-
Наше серверное приложение
-
Digidoc-узел (см. wsdl выше)
-
OCSP сервер, публикующий устаревшие или отозванные сертификаты id-карт
Если опустить очевидное, то процесс в общем выглядит так
-
Создание сессии между приложением и digidoc (StartSession) с передачей инфы о контейнере (который может включать несколько подписываемых файлов) — либо целиком файлы, либо их SHA1-хэш. Запоминаем вернувшися SessionCode у себя
-
Можно запросить с помощью GetSignatureModules сразу готовый html (со всякими апплетами, activex компонентами..) для того что-бы получить сертификаты клиента
-
Клиент авторизуется передавая данные серверу, который вызывает PrepareSignature (signCertHex, signCertId), получает обратно бинарный хэш контейнера документов SignedInfoDigest который клиент должен подписать
-
Клиент подписывает SignedInfoDigest введя PIN2 — генерируется подпись signValueHex и передаётся в FinalizeSignature(). На этом моменте digidoc проверяет действительность сертификата пользователя у OSCP
-
В успешном случае можно уже скачать .ddoc файл. Если оригинальные файлы не отсылались, то их в base64-форме внедряют в вернувшийся xml. Сессия закрывается CloseSession.
Авторизацию и подпись можно поставить и с помощью мобильного телефона, где добавляются ещё и сторона оператора (MSSP), но я этот случай здесь не рассматриваю.
О качестве ddservice
Поставляемый php-пакет в качестве примера полон багов и говнокода — написанный под php4 надо постараться что-бы прикрутить его к PEAR под php5, надо увеличить таймаут curl во всех запросах с 4 секунд на что-либо существенное.
У меня в процессе вылетали ошибкииспользовал test-среду вместо live 200: Failed to get signature confirmation/notary // используйте live среду вместо test Validation constraint violation: data type mismatch xsd:string in element 'Sesscode' //приведите сессию в int-тип
Чтение сертификата с ID-карты
Возвращаемся к двум основным пунктам подписи. До вызова PrepareSignature метода, на странице есть компонент (Applet, ActiveX либо зависимый от браузера plugin) который считывает два параметра
-
signCertHex — сертификат подписи переведённый из цифрового DER формата в HEX
-
signCertId — идентификатор приватного ключа
В качестве компонента DigiDocService возвращает ActiveX для IE (EIDCard.cab файлик) и аплет для остальных ( SignApplet_sig.jar,iaikPkcs11Wrapper_sig.jar ). ActiveX в плане интеграции довольно прост - можно javascriptом вызвать оба метода, следовательно прикрутить их к любому дизайну, другое дело что у меня этот компонент не работает (уж и драйверы переустанавливал - не помогло). А вот аплет мало того что подгружает яву, так и в дизайн не вписывается со своими кнопками.
Firefox - EstEID XPCOM v0.4
В идеале каждый браузер мог бы завести свой plugin который бы поддерживал обработку созданного в RIA
<object type="application/x-esteid" />
Но глядя как это тянется c 2006 года в Opera, очевидно что это будет долго. Впрочем Firefox уже умеет использовать onepin-opensc-pkcs11.dll как с помощью SK'шного хака, так и с помощью плагина EstEID XPCOM v0.4 написанного в RIA и Smartlink. В результате имеем красивую картинку в Firefox и нет нужды читать страшные мануалы по копированию dll-файлов и ошибок «Teie arvutisse on vaja installeerida PKCS#11 ohjurprogramm!»
Этот компонент успешно может яваскриптом выкачивать данные об id-карте к примеру— document.getElementById('esteid').signCert.cert. Но этот сертификат подписи для DigiDoc надо первести в шестнадцатиричный формат из PEM. Примерно так:
$sTempKey=str_replace(array( "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", "\n" ),'',$sTempKey); $sTempKey=base64_decode($sTempKey); $sTempKey=bin2hex($sTempKey);
EstID-плагин теперь переехал и выпускается как open-source
Internet explorer - EIDCard / dsiglite2 + idutil
EIDCard.cab компонент судя по существующему коду прост - создаёшь объект, ссылаешься на этот файл, потом через vbscript спрашиваешь сертификат или запрашиваешь подпись digest'а - всё как в Firefox. Что-бы он заработал нормально надо соблюдать следующие заповеди
- Выключить автоматическое удаление сертификатов в ID-card tool если вы под Windows Vista с правами администратора
- Держать объект EIDCard за пределами формы, иначе он не будет доступен вовсе
- При создании подписи VBscript по умолчанию пытается записать результат в поле signValueHex, вот только видимо кто-то использует такую же переменную в компоненте из-за чего подпись не передаётся серверу.
dsiglite2.cab это альтернатива для создания подписи, правда его найти удалось только у swedbank'а на странице и он слишком низкоуровневый, пришлось отказаться. Третий компонент очень полезен - idutil.cab понимает события вытаскивания карточки, хоть и с использованием JScript'а (в итоге получается каша из трёх ECMAscript диалектов)
<!--Использование idutil для чтения личных данных в IE--> <div id="mTag"></div> <OBJECT ID="myCard" CLASSID="CLSID:7F9F89F2-F12B-4B25-9C69-7358F38B898B" CodeBase="idutil.cab"></OBJECT> <SCRIPT LANGUAGE="JScript" FOR="myCard" EVENT="CardInserted() "> var mTag = document.getElementById("mTag"); mTag.innerHTML = "Reading data..."; timerId = window.setInterval("reading()",500); </SCRIPT> <SCRIPT LANGUAGE="JScript" FOR="myCard" EVENT="CardRemoved() "> var mTag = document.getElementById("mTag"); window.clearInterval(timerId); mTag.innerHTML = "Card removed"; </SCRIPT> <SCRIPT LANGUAGE="JScript"> var timerId; function reading(){ var mTag = document.getElementById("mTag"); window.clearInterval(timerId); mTag.innerHTML = ""; try { myCard.ReadCard(); mTag.innerHTML='Hello,'+myCard.familyName+' '+myCard.personalCode; } catch(e) { mTag.innerHTML="Reading failed"; } } </SCRIPT>
Если вы «переписываете» ddservice, то имеет смысл сохранить корень (wsdl класс) и переписать обёртку. Обратите внимание что после чтения сертификата и PREPARE, нельзя перенаправлять на другую страницу - надо делать POST на ту же самую страницу, иначе аплет будет ругаться. Вторая проблема - правильно заменять маркеры вида 1 на реальные данные. Ну и третья проблема - расставить пути к jar-файлам если у вас используется ЧПУ, иначе плагины будут искаться в локальной несуществующей папке.
Validity confirmation
Как я выше писал - существует digidoccheck с помощью которого можно проверить подпись. Листочек этот юридической силы не имеет в качестве распечатки цифровой подписи, но иметь на всякий случай не помешает. Разберёмся же какие тут данные есть и откуда они приходят
-
Данные о файле и пользователе - приходят в ответе FinalizeSignature. Там очень просто - ходи по массиву и выдёргивай что тебе надо.
-
Серийный номер сертификата - десятичная версия находится в ['SignedDocInfo']->SignatureInfo->Signer->Certificate->IssuerSerial и легко переводится в hex
-
Сертифицирующая сторона (собственно SK) в явном виде отсутсвует, надо выдирать из ['SignedDocInfo']->SignatureInfo->Confirmation->ResponderCertificate->Issuer
-
Хеш публичного ключа сертифицирующей стороны. На данный момент это ESTEID-SK 2007 и он есть на сайте, но правильней конечно запрашивать его динамически через WSDL метод GetSignersCertificate(). В php к результату надо приделать "-----BEGIN CERTIFICATE-----" и концовку (что-бы получить PEM формат) и взять от него openssl_x509_parse(). Внутри и будет находится заветная 4806DEBE ... Вариант захардкодить и обновлять каждые X лет я не рассматриваю
-
Хеш OCSP-сертификата о действительности карточки (HASH VALUE OF VALIDITY CONFIRMATION (OCSP RESPONSE)) берётся из WSDL метода getNotary() из которого OcspData попадает в sha1(base64_decode(...))
Ваш покорный слуга на время написания статьи нашёл мега-баг в том листочке что генерирует Digidoc Client - там OCSP responce hash всегда одинаковый. Кроме того я бы добавил хеши самих документов для печати, иначе слишком много зависимости от памяти SK. Ну и в подарок - метод hex2bin
function hex2bin($data) { $len = strlen($data); for($i=0;$i<$len;$i+=2) { $newdata .= pack("C",hexdec(substr($data,$i,2))); } return $newdata; }